package eztools

import (
	"crypto/md5"
	"encoding/json"
	"fmt"
	"net/url"
	"reflect"
	"strconv"
	"strings"
	"time"
)

const (
	LOCATION_TYPE_UNKNOWN = iota
	// areas from larger to smaller
	LOCATION_TYPE_COUNTRY
	LOCATION_TYPE_PROVINCE
	LOCATION_TYPE_CITY
	LOCATION_TYPE_DISTRICT
	LOCATION_TYPE_TOWN
	LOCATION_TYPE_STREET
	// areas below should be small enough to calculate direction
	LOCATION_TYPE_CBD
	LOCATION_TYPE_COMMUNITY
	LOCATION_TYPE_CROSSING
	LOCATION_TYPE_POI
	LOCATION_TYPE_NUMBER
	LOCATION_TYPE_MAX

	LOCATION_TYPE_STR_UNKNOWN   = "unknown"
	LOCATION_TYPE_STR_COUNTRY   = "country"
	LOCATION_TYPE_STR_PROVINCE  = "province"
	LOCATION_TYPE_STR_CITY      = "city"
	LOCATION_TYPE_STR_DISTRICT  = "district"
	LOCATION_TYPE_STR_TOWN      = "town"
	LOCATION_TYPE_STR_CBD       = "cbd"
	LOCATION_TYPE_STR_POI       = "poi"
	LOCATION_TYPE_STR_STREET    = "street"
	LOCATION_TYPE_STR_CROSSING  = "crossing"
	LOCATION_TYPE_STR_NUMBER    = "number"
	LOCATION_TYPE_STR_COMMUNITY = "community"

	MAP_REPLY_TYPE_STATUS   = "status"
	MAP_REPLY_TYPE_RESULTS  = "results"
	MAP_REPLY_TYPE_LEVEL    = "level"
	MAP_REPLY_TYPE_LOCATION = "location"
	MAP_REPLY_TYPE_LATI     = "lati"
	MAP_REPLY_TYPE_LONGI    = "long"
	MAP_REPLY_TYPE_DISTANCE = "distance"
	MAP_REPLY_TYPE_DURATION = "duration"

	MAP_REPLY_STRU_MAP    = "map"
	MAP_REPLY_STRU_MAPSLC = "slicedmap"

	PATH_TYPE_STR_WALK = "walk"

	// MAP_LALO_TYPE_STR_LALO = latitude<separator>longitude
	MAP_LALO_TYPE_STR_LALO = "lalo"
	// MAP_LALO_TYPE_STR_LALO = longitude<separator>latitude
	MAP_LALO_TYPE_STR_LOLA = "lola"
)

type cfgEZtoolsMapReply struct {
	Cmt   string               `xml:",comment"`
	Type  string               `xml:"type,attr"`
	Name  string               `xml:"name,attr"`
	Stru  string               `xml:"stru,attr"`
	Reply []cfgEZtoolsMapReply `xml:"reply"`
	Text  string               `xml:",chardata"`
}

type CfgEZtoolsMap struct {
	Cmt       string `xml:",comment"`
	Name      string `xml:"name,attr"`
	Key       string `xml:"key,attr"`
	Separator string `xml:"separator,attr"`
	PathOri   string `xml:"ori,attr"`
	PathDst   string `xml:"dst,attr"`
	Md5Key    string `xml:"md5Key,attr"`
	Md5Pref   string `xml:"md5Pref,attr"`
	Lalo      string `xml:"lalo,attr"`
	TimeStamp string `xml:"timestamp,attr"`
	// Text is not used
	Text   string `xml:",chardata"`
	Locate struct {
		Cmt     string               `xml:",comment"`
		UrlHost string               `xml:"urlHost,attr"`
		UrlPath string               `xml:"urlPath,attr"`
		Addr    string               `xml:"addr,attr"`
		Reply   []cfgEZtoolsMapReply `xml:"reply"`
		// Text is not used
		Text string `xml:",chardata"`
	} `xml:"locate"`
	Levels struct {
		Cmt string `xml:",comment"`
		// Text is not used
		Text  string `xml:",chardata"`
		Level []struct {
			Cmt  string `xml:",comment"`
			Type string `xml:"type,attr"`
			Text string `xml:",chardata"`
		} `xml:"level"`
	} `xml:"levels"`
	lvlPairs PairsIS
	Paths    []struct {
		Cmt     string               `xml:",comment"`
		Type    string               `xml:"type,attr"`
		UrlHost string               `xml:"urlHost,attr"`
		UrlPath string               `xml:"urlPath,attr"`
		Reply   []cfgEZtoolsMapReply `xml:"reply"`
		// Text is not used
		Text string `xml:",chardata"`
	} `xml:"path"`
}

type PathInfos struct {
	// there may be more members in this struct,
	// so use member names during instance creation
	Distance float32
}

func (path *PathInfos) SetDistance(v interface{}) {
	var distance float64
	vs, ok := v.(string)
	if ok {
		if len(vs) < 1 {
			return
		}
		distance, _ = strconv.ParseFloat(vs, 32)
	} else {
		v3, ok := v.(float32)
		if ok {
			path.Distance = v3
			return
		}
		distance, ok = v.(float64)
		if !ok {
			return
		}
	}
	path.Distance = float32(distance)
}

type LocationInfos struct {
	// there may be more members in this struct,
	// so use member names during instance creation
	Latitude, Longitude float32
	LevelInt            int
	LevelStr            string
}

func (loc LocationInfos) String(separator, order string) string {
	switch order {
	case MAP_LALO_TYPE_STR_LOLA:
		return fmt.Sprintf("%f%s%f",
			loc.Longitude, separator, loc.Latitude)
	default:
		return fmt.Sprintf("%f%s%f",
			loc.Latitude, separator, loc.Longitude)
	}
}

// ParseLaLo sets a string or float64 to latitude or longitude
func (loc *LocationInfos) ParseLaLo(in interface{}, lalo string) bool {
	lf, ok := in.(float64)
	if !ok {
		str, ok := in.(string)
		if !ok {
			if Debugging && Verbose > 1 {
				Log(reflect.TypeOf(in).String() +
					" got instead of float or string")
			}
			return false
		}
		lf, _ = strconv.ParseFloat(str, 32)
	}
	switch lalo {
	case MAP_REPLY_TYPE_LATI:
		loc.Latitude = float32(lf)
	case MAP_REPLY_TYPE_LONGI:
		loc.Longitude = float32(lf)
	}
	return true
}

func (loc *LocationInfos) Parse(in interface{}, separator, order string) bool {
	str, ok := in.(string)
	if !ok {
		if Debugging && Verbose > 1 {
			Log(reflect.TypeOf(in).String() +
				" got instead of string")
		}
		return false
	}
	slc := strings.Split(str, separator)
	if len(slc) < 2 {
		return false
	}
	first, _ := strconv.ParseFloat(slc[0], 32)
	second, _ := strconv.ParseFloat(slc[1], 32)
	switch order {
	case MAP_LALO_TYPE_STR_LOLA:
		loc.Latitude, loc.Longitude = float32(second), float32(first)
	default:
		loc.Latitude, loc.Longitude = float32(first), float32(second)
	}
	return true
}

func (loc *LocationInfos) SetLevel(str string, val int) {
	if len(str) < 1 {
		return
	}
	loc.LevelStr, loc.LevelInt = str, val
}

func calcMd5(key string) string {
	ret := md5.Sum([]byte(url.QueryEscape(key)))
	return fmt.Sprintf("%x", ret)
}

// Return values:
//	LOCATION_TYPE_MAX if wrong type or unrecognized
func (cfg CfgEZtoolsMap) parseLevel(in interface{}) (str string, ret int) {
	ret = LOCATION_TYPE_MAX
	str, ok := in.(string)
	if !ok {
		if Debugging && Verbose > 1 {
			Log(reflect.TypeOf(in).String() +
				" got instead of string")
		}
		return
	}
	for _, l := range cfg.Levels.Level {
		if len(l.Type) < 1 {
			continue
		}
		if l.Text == str {
			if cfg.lvlPairs.Len() < 1 {
				cfg.lvlPairs.Add(LOCATION_TYPE_NUMBER,
					LOCATION_TYPE_STR_NUMBER)
				cfg.lvlPairs.Add(LOCATION_TYPE_COMMUNITY,
					LOCATION_TYPE_STR_COMMUNITY)
				cfg.lvlPairs.Add(LOCATION_TYPE_CROSSING,
					LOCATION_TYPE_STR_CROSSING)
				cfg.lvlPairs.Add(LOCATION_TYPE_CBD,
					LOCATION_TYPE_STR_CBD)
				cfg.lvlPairs.Add(LOCATION_TYPE_CITY,
					LOCATION_TYPE_STR_CITY)
				cfg.lvlPairs.Add(LOCATION_TYPE_COUNTRY,
					LOCATION_TYPE_STR_COUNTRY)
				cfg.lvlPairs.Add(LOCATION_TYPE_DISTRICT,
					LOCATION_TYPE_STR_DISTRICT)
				cfg.lvlPairs.Add(LOCATION_TYPE_POI,
					LOCATION_TYPE_STR_POI)
				cfg.lvlPairs.Add(LOCATION_TYPE_PROVINCE,
					LOCATION_TYPE_STR_PROVINCE)
				cfg.lvlPairs.Add(LOCATION_TYPE_TOWN,
					LOCATION_TYPE_STR_TOWN)
				cfg.lvlPairs.Add(LOCATION_TYPE_STREET,
					LOCATION_TYPE_STR_STREET)
			}
			found, err := cfg.lvlPairs.FindStr(l.Type)
			if err != nil {
				if Debugging && Verbose > 1 {
					LogErrPrint(err)
				}
			} else {
				ret = found
			}
		}
	}
	return
}

func (cfg CfgEZtoolsMap) parseReply(cfgLvl []cfgEZtoolsMapReply,
	json interface{}, in interface{},
	funcNew func() interface{},
	funcIng func(interface{}, *cfgEZtoolsMapReply, interface{}) bool,
	funcEd func(lcl, sub bool,
		in interface{}, inf interface{}) (interface{}, error)) (
	out interface{}, err error) {
	out = in
	res, ok := json.(map[string]interface{})
	if !ok {
		if Debugging && Verbose > 1 {
			Log(reflect.TypeOf(json).String() +
				" got instead of map")
		}
		return out, ErrOutOfBound
	}
	var foundLcl, foundSub bool
	inf := funcNew()
	for i, v := range res {
		if Debugging && Verbose > 2 {
			LogPrint(cfgLvl, "[", i, "]", "=", v)
		}
		var cfgLvl1 *cfgEZtoolsMapReply
		for _, c := range cfgLvl {
			if i == c.Name {
				cfgLvl1 = &c
				break
			}
		}
		if cfgLvl1 == nil {
			continue
		}
		switch cfgLvl1.Stru {
		case MAP_REPLY_STRU_MAPSLC:
			// should I care about type="results"?
			slc, ok := v.([]interface{})
			if !ok {
				if Debugging && Verbose > 1 {
					Log(reflect.TypeOf(v).String() +
						" got instead of slice")
				}
			} else {
				for _, slc1 := range slc {
					out, err = cfg.parseReply(cfgLvl1.Reply,
						slc1, out, funcNew, funcIng, funcEd)
					if err == nil {
						foundSub = true
					}
				}
			}

		default:
			switch cfgLvl1.Type {
			case MAP_REPLY_TYPE_RESULTS:
				out, err = cfg.parseReply(cfgLvl1.Reply,
					v, out, funcNew, funcIng, funcEd)
				if err == nil {
					foundSub = true
				}
			case MAP_REPLY_TYPE_STATUS:
				vs, ok := v.(string)
				if !ok {
					vi, ok := v.(float64)
					if !ok {
						if Debugging && Verbose > 1 {
							Log(reflect.TypeOf(v).String() +
								" got instead of string or float")
						}
					} else {
						vf, _ := strconv.ParseFloat(cfgLvl1.Text, 64)
						if vi != vf {
							return nil, ErrNoValidResults
						}
					}
				} else {
					if vs != cfgLvl1.Text {
						return nil, ErrNoValidResults
					}
				}
			default:
				foundLcl = funcIng(v, cfgLvl1, inf) || foundLcl
				/*i, ok := inf.(*PathInfos)
				if !ok {
					Log(reflect.TypeOf(inf).String() +
						" got instead of addr of path")
				} else {
					LogPrint(i)
				}*/
			}
		}
	}
	/*if true {
		out, err = funcEd(foundLcl, foundSub, out, inf)
		Log("looped=", out)
		return out, err
	}*/
	return funcEd(foundLcl, foundSub, out, inf)
}

// Return values:
//	ErrOutOfBound if json fails to parse
//	ErrNoValidResults if status is failure in json
func (cfg CfgEZtoolsMap) parseLocation(cfgLvl []cfgEZtoolsMapReply,
	json interface{}, in []LocationInfos) (out []LocationInfos, err error) {
	ret, err := cfg.parseReply(cfgLvl, json, in,
		func() interface{} {
			return new(LocationInfos)
		},
		func(v interface{}, cfgReply *cfgEZtoolsMapReply, inf interface{}) bool {
			infT, ok := inf.(*LocationInfos)
			if !ok {
				if Debugging && Verbose > 1 {
					Log(reflect.TypeOf(inf).String() +
						" got instead of Location pointer")
				}
				return false
			}
			switch cfgReply.Type {
			case MAP_REPLY_TYPE_LEVEL:
				infT.SetLevel(cfg.parseLevel(v))
			case MAP_REPLY_TYPE_LATI, MAP_REPLY_TYPE_LONGI:
				infT.ParseLaLo(v, cfgReply.Type)
				return true
			case MAP_REPLY_TYPE_LOCATION:
				infT.Parse(v, cfg.Separator, cfg.Lalo)
				// as long as we have tried to parse real position,
				// the result is returned
				return true
			}
			return false
		},
		func(lcl, sub bool, in interface{}, inf interface{}) (interface{}, error) {
			ins, ok := in.([]LocationInfos)
			if !ok {
				if Debugging && Verbose > 1 {
					Log(reflect.TypeOf(in).String() +
						" got instead of slice")
				}
				return in, nil
			}
			inp, ok := inf.(*LocationInfos)
			if !ok {
				if Debugging && Verbose > 1 {
					Log(reflect.TypeOf(inf).String() +
						" got instead of location")
				}
				return in, nil
			}
			if !lcl {
				if sub {
					if len(inp.LevelStr) > 0 {
						// sub level has progress,
						// let's change it
						ins[len(ins)-1].LevelStr,
							ins[len(ins)-1].LevelInt =
							inp.LevelStr, inp.LevelInt
					}
					return ins, nil
				}
				return in, ErrNoValidResults
			}
			/*if true {
				t := append(ins, *inp)
				Log("appended=", t)
				return t, nil
			}*/
			return append(ins, *inp), nil

		})
	if err != nil || ret == nil {
		return nil, err
	}
	out, ok := ret.([]LocationInfos)
	if !ok {
		if Debugging && Verbose > 1 {
			Log(reflect.TypeOf(ret).String() +
				" got instead of slice of location")
		}
		return nil, ErrOutOfBound
	}
	return
}

// Return values:
//	ErrOutOfBound if json fails to parse
//	ErrNoValidResults if status is failure in json
func (cfg CfgEZtoolsMap) parsePathWalk(cfgLvl []cfgEZtoolsMapReply,
	json interface{}, in []PathInfos) (out []PathInfos, err error) {
	ret, err := cfg.parseReply(cfgLvl, json, in,
		func() interface{} {
			return new(PathInfos)
		},
		func(v interface{}, cfgReply *cfgEZtoolsMapReply, inf interface{}) bool {
			infT, ok := inf.(*PathInfos)
			if !ok {
				if Debugging && Verbose > 1 {
					Log(reflect.TypeOf(inf).String() +
						" got instead of Path pointer")
				}
				return false
			}
			switch cfgReply.Type {
			case MAP_REPLY_TYPE_DISTANCE:
				infT.SetDistance(v)
				return true
				//case MAP_REPLY_TYPE_DURATION:
			}
			return false
		},
		func(lcl, sub bool, in interface{}, inf interface{}) (interface{}, error) {
			ins, ok := in.([]PathInfos)
			if !ok {
				if Debugging && Verbose > 1 {
					Log(reflect.TypeOf(in).String() +
						" got instead of slice")
				}
				return in, nil
			}
			inp, ok := inf.(*PathInfos)
			if !ok {
				if Debugging && Verbose > 1 {
					Log(reflect.TypeOf(inf).String() +
						" got instead of path")
				}
				return in, nil
			}
			if !lcl {
				return in, nil
			}
			/*if true {
				t := append(ins, *inp)
				Log("appended=", t)
				return t, nil
			}*/
			return append(ins, *inp), nil

		})
	if err != nil || ret == nil {
		return nil, err
	}
	out, ok := ret.([]PathInfos)
	if !ok {
		if Debugging && Verbose > 1 {
			Log(reflect.TypeOf(ret).String() +
				" got instead of slice of path")
		}
		return nil, ErrOutOfBound
	}
	return
}

// Addr2Location returns location info from address string
func (cfg CfgEZtoolsMap) Addr2Location(addr string) ([]LocationInfos, error) {
	locate := cfg.Locate
	if len(cfg.Key) < 0 || len(locate.Addr) < 0 || len(locate.UrlHost) < 0 {
		return nil, ErrInvalidInput
	}
	uri := locate.UrlPath + cfg.Key + "&" + locate.Addr + addr
	// TODO: url encode?
	if len(cfg.Md5Key) > 0 && len(cfg.Md5Pref) > 0 {
		uri += "&" + cfg.Md5Pref + calcMd5(uri+cfg.Md5Key)
	}
	body, _, err := RestGet(locate.UrlHost+uri, AuthInfo{}, nil)
	if err != nil {
		return nil, err
	}
	if body == nil {
		return nil, ErrNoValidResults
	}
	bodyBytes, ok := body.([]byte)
	if ok {
		err = json.Unmarshal(bodyBytes, &body)
		if err != nil {
			return nil, err
		}
	}
	if body == nil {
		return nil, nil
	}
	return cfg.parseLocation(locate.Reply, body, nil)
}

// returns location info from address string
func (cfg CfgEZtoolsMap) CalcRouteWalk(loc [2]LocationInfos) (
	[]PathInfos, error) {
	if len(cfg.Key) < 0 || cfg.Paths == nil ||
		len(cfg.PathOri) < 0 || len(cfg.PathDst) < 0 {
		return nil, ErrInvalidInput
	}
	for _, path := range cfg.Paths {
		if path.Type != PATH_TYPE_STR_WALK {
			continue
		}
		uri := path.UrlPath + cfg.Key + "&" +
			cfg.PathOri +
			loc[0].String(cfg.Separator, cfg.Lalo) +
			"&" +
			cfg.PathDst +
			loc[1].String(cfg.Separator, cfg.Lalo)
		if len(cfg.TimeStamp) > 0 {
			uri += "&" + cfg.TimeStamp +
				"=" + strconv.FormatUint(uint64(time.Now().Unix()), 10)
		}
		//uri = url.PathEscape(uri)
		// TODO: url encode?
		if len(cfg.Md5Key) > 0 && len(cfg.Md5Pref) > 0 {
			uri += "&" + cfg.Md5Pref + calcMd5(uri+cfg.Md5Key)
		}
		body, _, err := RestGet(path.UrlHost+uri, AuthInfo{}, nil)
		if err != nil {
			return nil, err
		}
		if body == nil {
			return nil, ErrNoValidResults
		}
		bodyBytes, ok := body.([]byte)
		if ok {
			err = json.Unmarshal(bodyBytes, &body)
			if err != nil {
				return nil, err
			}
		}
		if body == nil {
			return nil, nil
		}
		//LogPrint(body)
		return cfg.parsePathWalk(path.Reply, body, nil)
	}
	return nil, ErrOutOfBound
}
